//  Copyright (c) 2005-2017 Hartmut Kaiser
//  Copyright (c)      2011 Bryce Lelbach
//
//  SPDX-License-Identifier: BSL-1.0
//  Distributed under the Boost Software License, Version 1.0. (See accompanying
//  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <pika/config.hpp>
#include <pika/assert.hpp>
#include <pika/ini/ini.hpp>
#include <pika/logging.hpp>
#include <pika/modules/errors.hpp>
#include <pika/runtime_configuration/init_ini_data.hpp>
#include <pika/version.hpp>

#include <boost/tokenizer.hpp>

#include <algorithm>
#include <filesystem>
#include <iostream>
#include <map>
#include <memory>
#include <random>
#include <string>
#include <system_error>
#include <utility>
#include <vector>

///////////////////////////////////////////////////////////////////////////////
namespace pika::util {
    ///////////////////////////////////////////////////////////////////////////
    bool handle_ini_file(pika::detail::section& ini, std::string const& loc)
    {
        try
        {
            std::error_code ec;
            if (!std::filesystem::exists(loc, ec) || ec)
                return false;    // avoid exception on missing file
            ini.read(loc);
        }
        catch (pika::exception const& /*e*/)
        {
            return false;
        }
        return true;
    }

    ///////////////////////////////////////////////////////////////////////////
    // NOLINTBEGIN(bugprone-easily-swappable-parameters)
    bool handle_ini_file_env(
        pika::detail::section& ini, char const* env_var, char const* file_suffix)
    // NOLINTEND(bugprone-easily-swappable-parameters)
    {
        char const* env = getenv(env_var);
        if (nullptr != env)
        {
            std::filesystem::path inipath(env);
            if (nullptr != file_suffix) inipath /= std::filesystem::path(file_suffix);

            if (handle_ini_file(ini, inipath.string()))
            {
                PIKA_LOG(info, "loaded configuration (${{{}}}): {}", env_var, inipath.string());
                return true;
            }
        }
        return false;
    }

    ///////////////////////////////////////////////////////////////////////////
    // read system and user specified ini files
    //
    // returns true if at least one alternative location has been read
    // successfully
    bool init_ini_data_base(pika::detail::section& ini, std::string& pika_ini_file)
    {
        // fall back: use compile time prefix
        std::string ini_paths(ini.get_entry("pika.master_ini_path"));
        std::string ini_paths_suffixes(ini.get_entry("pika.master_ini_path_suffixes"));

        // split off the separate paths from the given path list
        using tokenizer_type = boost::tokenizer<boost::char_separator<char>>;

        boost::char_separator<char> sep(PIKA_INI_PATH_DELIMITER);
        tokenizer_type tok_paths(ini_paths, sep);
        tokenizer_type::iterator end_paths = tok_paths.end();
        tokenizer_type tok_suffixes(ini_paths_suffixes, sep);
        tokenizer_type::iterator end_suffixes = tok_suffixes.end();

        bool result = false;
        for (tokenizer_type::iterator it = tok_paths.begin(); it != end_paths; ++it)
        {
            for (tokenizer_type::iterator jt = tok_suffixes.begin(); jt != end_suffixes; ++jt)
            {
                std::string path = *it;
                path += *jt;
                bool result2 = handle_ini_file(ini, path + "/pika.ini");
                if (result2) { PIKA_LOG(info, "loaded configuration: {}/pika.ini", path); }
                result = result2 || result;
            }
        }

        // look in the current directory first
        std::string cwd = std::filesystem::current_path().string() + "/.pika.ini";
        {
            bool result2 = handle_ini_file(ini, cwd);
            if (result2) { PIKA_LOG(info, "loaded configuration: {}", cwd); }
            result = result2 || result;
        }

        // look for master ini in the PIKA_INI environment
        result = handle_ini_file_env(ini, "PIKA_INI") || result;

        // afterwards in the standard locations
#if !defined(PIKA_WINDOWS)    // /etc/pika.ini doesn't make sense for Windows
        {
            bool result2 = handle_ini_file(ini, "/etc/pika.ini");
            if (result2) { PIKA_LOG(info, "loaded configuration: /etc/pika.ini"); }
            result = result2 || result;
        }
#endif

        result = handle_ini_file_env(ini, "HOME", ".pika.ini") || result;
        result = handle_ini_file_env(ini, "PWD", ".pika.ini") || result;

        if (!pika_ini_file.empty())
        {
            std::error_code ec;
            if (!std::filesystem::exists(pika_ini_file, ec) || ec)
            {
                std::cerr << "pika::init: command line warning: file specified using --pika:config "
                             "does not exist ("
                          << pika_ini_file << ")." << std::endl;
                pika_ini_file.clear();
                result = false;
            }
            else
            {
                bool result2 = handle_ini_file(ini, pika_ini_file);
                if (result2) { PIKA_LOG(info, "loaded configuration: {}", pika_ini_file); }
                return result || result2;
            }
        }
        return result;
    }

    ///////////////////////////////////////////////////////////////////////////
    // global function to read component ini information
    void merge_component_inis(pika::detail::section& ini)
    {
        // now merge all information into one global structure
        std::string ini_path(ini.get_entry("pika.ini_path"));
        std::vector<std::string> ini_paths;

        // split off the separate paths from the given path list
        using tokenizer_type = boost::tokenizer<boost::char_separator<char>>;

        boost::char_separator<char> sep(PIKA_INI_PATH_DELIMITER);
        tokenizer_type tok(ini_path, sep);
        tokenizer_type::iterator end = tok.end();
        for (tokenizer_type::iterator it = tok.begin(); it != end; ++it) ini_paths.push_back(*it);

        // have all path elements, now find ini files in there...
        std::vector<std::string>::iterator ini_end = ini_paths.end();
        for (std::vector<std::string>::iterator it = ini_paths.begin(); it != ini_end; ++it)
        {
            try
            {
                std::filesystem::directory_iterator nodir;
                std::filesystem::path this_path(*it);

                std::error_code ec;
                if (!std::filesystem::exists(this_path, ec) || ec) continue;

                for (std::filesystem::directory_iterator dir(this_path); dir != nodir; ++dir)
                {
                    if (dir->path().extension() != ".ini") continue;

                    // read and merge the ini file into the main ini hierarchy
                    try
                    {
                        ini.merge(dir->path().string());
                        PIKA_LOG(info, "loaded configuration: {}", dir->path().string());
                    }
                    catch (pika::exception const& /*e*/)
                    {
                        ;
                    }
                }
            }
            catch (std::filesystem::filesystem_error const& /*e*/)
            {
                ;
            }
        }
    }

    namespace detail {
        inline bool cmppath_less(std::pair<std::filesystem::path, std::string> const& lhs,
            std::pair<std::filesystem::path, std::string> const& rhs)
        {
            return lhs.first < rhs.first;
        }

        inline bool cmppath_equal(std::pair<std::filesystem::path, std::string> const& lhs,
            std::pair<std::filesystem::path, std::string> const& rhs)
        {
            return lhs.first == rhs.first;
        }
    }    // namespace detail
}    // namespace pika::util
